package mmc.cell3;

import static mmc.util.MyMath.floor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import mmc.Constants;
import mmc.EngineObject;
import mmc.cell.Block;
import mmc.cell.BlockIterator;
import mmc.cell.CellPosition;
import mmc.cell.ServerCell;
import mmc.cell.ServerCellStorage;
import mmc.server.Server;

public class ServerCellManagerImpl implements mmc.cell.ServerCellManager{
	Map<CellPosition, ServerChunk> loadedChunks = new TreeMap<CellPosition, ServerChunk>();
	ServerCellStorage storage;
	RangeSweeper sweeper;
	ServerChunkGenerator chunkGenerator;
	Set<ServerCell> immediateDirtyCells = new TreeSet<ServerCell>();
	
	public ServerCellManagerImpl(ServerCellStorage storage){
		this.storage=storage;
		chunkGenerator = new ServerChunkGenerator();
		
		loadChunk(new CellPosition(0, 0, 0));
		loadChunk(new CellPosition(-Constants.CHUNK_BLOCKS, 0, 0));
		loadChunk(new CellPosition(-Constants.CHUNK_BLOCKS, 0, -Constants.CHUNK_BLOCKS));
		loadChunk(new CellPosition(0, 0, -Constants.CHUNK_BLOCKS));
		loadChunk(new CellPosition(0, -Constants.CHUNK_BLOCKS, 0));
		loadChunk(new CellPosition(-Constants.CHUNK_BLOCKS, -Constants.CHUNK_BLOCKS, 0));
		loadChunk(new CellPosition(-Constants.CHUNK_BLOCKS, -Constants.CHUNK_BLOCKS, -Constants.CHUNK_BLOCKS));
		loadChunk(new CellPosition(0, -Constants.CHUNK_BLOCKS, -Constants.CHUNK_BLOCKS));
		
		sweeper = new RangeSweeper(this);
	}
	
	@Override
	public void update(Server server, float dt, Set<ServerCell> dirtyCells){
		dirtyCells.addAll(immediateDirtyCells);
		
		synchronized(dirtyCells){
			for(ServerCell c : dirtyCells){
				if(!c.isCompressed()){
					for(int i=0;i<Constants.CELL_SIZE;i++){
						for(int j=Constants.CELL_SIZE-1;j>=0;j--){ //light travels downwards further than upwards, so this is neccessary (although not memory optimal)
							for(int k=0;k<Constants.CELL_SIZE;k++){
								
								byte b = c.block[i*Constants.CELL_SIZE2+j*Constants.CELL_SIZE+k];
								if(b>Block.AIR_SUN && b<=Block.AIR_DARKNESS)
									updateLightRec(c.pos.x + i, c.pos.y + j, c.pos.z + k, 0);
							}
						}
					}
				}
			}
		}
		
		immediateDirtyCells.removeAll(dirtyCells);
	}
	
	public boolean isChunkLoaded(CellPosition pos){
		return loadedChunks.get(pos)!=null;
	}
	
	public void assertChunkLoaded(CellPosition pos){
		if(loadedChunks.get(pos)==null){
			loadChunk(new CellPosition(pos.x, pos.y, pos.z));
		}
	}
		
	public void loadChunk(ServerChunk chunk){
		synchronized(loadedChunks){
			loadedChunks.put(chunk.pos, chunk);
		}
		
		if(Server.instance.getScene()!=null)
			Server.instance.getScene().markCellsAsDirty(chunk.cells);
	}
	
	public void loadChunk(CellPosition pos){
		loadChunk(chunkGenerator.generateChunk(pos));
	}

	public Map<CellPosition, ServerChunk> getLoadedChunks() {
		return loadedChunks;
	}
	
	public BlockIterator getBlocksNearObject(EngineObject obj, float maxDistance){
		BlockIterator it = new BlockIteratorImpl();
		
		synchronized(loadedChunks){
			for(ServerChunk c : loadedChunks.values()){
				if(c.isContainingBlockInRange(obj, maxDistance)){
					synchronized(c.cells){
						for(ServerCell cell : c.cells){
							if(cell.isContainingBlockInRange(obj, maxDistance)){
								it.addAffectedChunk(cell);
								
								for(int bx=0;bx<Constants.CELL_SIZE;bx++)
									for(int by=0;by<Constants.CELL_SIZE;by++)
										for(int bz=0;bz<Constants.CELL_SIZE;bz++){
											if(cell.isBlockInRange(obj, maxDistance, bx, by, bz))
												it.addDefinition(cell, bx, by, bz);
										}
							}
						}
					}
				}
			}
		}
		
		return it;
	}

	@Override
	public void addCellsInRange(EngineObject o, float range, Set<ServerCell> currentlyKnown, Set<ServerCell> newCells) {
		synchronized(loadedChunks){
			for(ServerChunk chunk: loadedChunks.values()){
				if(chunk.isContainingBlockInRange(o, range)){
					for(ServerCell c : chunk.getCells()){
						if(c.isContainingBlockInRange(o, range)&&!currentlyKnown.contains(c)){
							newCells.add(c);
						}
					}
				}
			}
		}
	}

	public ServerChunk getChunkSafe(int x, int y, int z){
		return loadedChunks.get(new CellPosition(x&Constants.CHUNK_MASK,y&Constants.CHUNK_MASK,z&Constants.CHUNK_MASK));
	}
	
	public ServerCell getCellSafe(CellPosition p){
		return getCellSafe(p.x, p.y, p.z);
	}
	
	public ServerCell getCellSafe(int x, int y, int z){
		ServerChunk chunk = getChunkSafe(x,y,z);
		
		if(chunk==null)
			return null;
		
		x = (x&Constants.CHUNK_CELL_MASK) / Constants.CELL_SIZE;
		y = (y&Constants.CHUNK_CELL_MASK) / Constants.CELL_SIZE;
		z = (z&Constants.CHUNK_CELL_MASK) / Constants.CELL_SIZE;
		
		return chunk.cells.get(x*Constants.CHUNK_CELLS*Constants.CHUNK_CELLS + 
				               y*Constants.CHUNK_CELLS + 
				               z);
	}
	
	public byte getBlock(int x, int y, int z) {
		ServerCell c = getCellSafe(x, y, z);
		if(c==null){
			return Block.AIR_SUN;
		}else{
			if(c.isCompressed()){
				return (byte)c.solid;
			}else
				return c.block[(x - c.pos.x)*Constants.CELL_SIZE2+(y - c.pos.y)*Constants.CELL_SIZE+(z - c.pos.z)];
		}
	}
	
	public void setBlock_noCheck(int x, int y, int z, byte value) {
		ServerCell c = getCellSafe(x, y, z);
		
		if(c==null){
			throw new RuntimeException("internal error!");
		}else{
			if(c.isCompressed())
				c.decompress();
			
			immediateDirtyCells.add(c);
			c.block[(x - c.pos.x)*Constants.CELL_SIZE2+(y - c.pos.y)*Constants.CELL_SIZE+(z - c.pos.z)] = value;
		}
	}
	
	@Override
	public byte getEnclosingBlock(float x, float y, float z) {
		int ix = floor(x),
	        iy = floor(y),
	        iz = floor(z);
		
		ServerCell c = getCellSafe(ix, iy, iz);
		if(c==null){
			return Block.AIR_SUN;
		}else{
			if(c.isCompressed()){
				return (byte)c.solid;
			}else
				return c.block[(ix - c.pos.x)*Constants.CELL_SIZE2+(iy - c.pos.y)*Constants.CELL_SIZE+(iz - c.pos.z)];
		}
	}
	
	private void updateLightRec(int x, int y, int z, int d){
		boolean changed = false;
		byte id = getBlock(x,y,z), neighborId;
		
		if(id>Block.AIR_SUN && id<=Block.AIR_DARKNESS){
			
			neighborId = getBlock(x,y+1,z);
			if(neighborId==Block.AIR_SUN){
				id = Block.AIR_SUN;
				changed = true;
				
				//direct sunlight contact - spread downwards until surface is reached
				for(int j=y-1;getBlock(x,j,z)<=Block.AIR_DARKNESS;j--){
					setBlock_noCheck(x,j,z,Block.AIR_SUN);
				}
				
			}else{
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
				
				neighborId = getBlock(x+1,y,z);
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
				
				neighborId = getBlock(x-1,y,z);
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
				
				neighborId = getBlock(x,y-1,z);
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
				
				neighborId = getBlock(x,y,z+1);
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
				
				neighborId = getBlock(x,y,z-1);
				if(neighborId<id-1){
					id = (byte) (neighborId+1);
					changed = true;
				}
			}
			
			if(changed){
				setBlock_noCheck(x,y,z,id);
				
				if(d<Constants.MAX_LIGHT_UPDATE_BLOCKS){
					updateLightRec(x+1, y, z, d+1);
					updateLightRec(x-1, y, z, d+1);
					updateLightRec(x, y+1, z, d+1);
					updateLightRec(x, y-1, z, d+1);
					updateLightRec(x, y, z+1, d+1);
					updateLightRec(x, y, z-1, d+1);
				}
			}
		}
	}

	@Override
	public ServerCell[] getSpawnPackage(EngineObject spawnPoint){
		ArrayList<ServerCell> al = new ArrayList<ServerCell>();
		ServerCell c;
		CellPosition p = new CellPosition(spawnPoint);
		
		while((c = getCellSafe(p))!=null){
			al.add(c);
			
			p.y -= Constants.CELL_SIZE;
			
			if(c.isCompressedAndSolid())
				return al.toArray(new ServerCell[al.size()]);
		}

		throw new RuntimeException("could not find solid block for spawn package ("+spawnPoint+")");
	}

	@Override
	public Collection<ServerCell> debug_getAllCells() {
		List<ServerCell> allCells = new ArrayList<ServerCell>(Constants.CHUNK_CELLS*Constants.CHUNK_CELLS*Constants.CHUNK_CELLS*loadedChunks.size());
		
		synchronized(loadedChunks){
			for(ServerChunk c : loadedChunks.values()){
				synchronized(c){
					allCells.addAll(c.cells);
				}
			}
		}
		
		return allCells;
	}

	@Override
	public void debug_setBlock(int x, int y, int z, byte value) {
		ServerCell c = getCellSafe(x,y,z);
		
		if(c==null){
			throw new RuntimeException("Cell "+x+"/"+y+"/"+z+" not loaded!");
		}
		
		c.setValue(x&Constants.BLOCK_MASK, y&Constants.BLOCK_MASK, z&Constants.BLOCK_MASK, value);
		
		List<ServerCell> l = new ArrayList<ServerCell>(1);
		l.add(c);
		Server.instance.getScene().markCellsAsDirty(l);
	}
	
	public ServerChunkGenerator getChunkGenerator(){
		return chunkGenerator;
	}

	@Override
	public void shutdown() {
		sweeper.quit();
	}

	@Override
	public void debug_compress() {
		synchronized(loadedChunks){
			for(ServerChunk c : loadedChunks.values()){
				c.compress();
			}
		}
	}
}
